/*
 * Copyright 2006-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.martinimix.service.syndication.feedburner;

import java.io.IOException;
import java.util.List;

import net.martinimix.domain.syndication.FeedBurnerFeed;
import net.martinimix.domain.syndication.SyndicationFeed;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.output.XMLOutputter;

/**
 * Provides a <a href="http://www.feedburner.com">FeedBurner</a> syndication
 * service.
 * 
 * <p>
 * FeedBurner does not provide the capability publish feeds directly to its
 * service.  It relies on the availability of an RSS or Atom feed at another
 * publicly accessible URL.
 * </p>
 * 
 * <p>
 * FeedBurner does provide public feed hosting and a wealth of feed related services.
 * However, since it cannot receive publish events directly, MartiniMix provides
 * a FeedBurner integration as syndication feed publish listener rather than
 * a true feed publishing service.
 * </p>
 * 
 * <p>
 * Despite this small complication, syndicating content with FeedBurner is very straight
 * foward.  All that is required is a FeedBurner account and the presence of a
 * {@link FeedBurnerSyndicationEventListener} bean configured in your Spring container.
 * </p> 
 * 
 * @author Scott Rossillo
 *
 */
public class FeedBurnerService {
	
	/**
	 * FeedBurner's API base URL.
	 */
	protected final static String BASE_API_URL = "http://api.feedburner.com/management/1.0/";
	
	/**
	 * FeedBurner's add API URL. 
	 */
	protected final static String ADD_API_URL = BASE_API_URL + "AddFeed";
	
	/**
	 * FeedBurner's re-synchronization API URL.
	 */
	protected final static String RESYNC_API_URL = BASE_API_URL + "ResyncFeed";
	
	
	/**
	 * FeedBurner's delete API URL.
	 */
	protected final static String DELETE_API_URL = BASE_API_URL + "DeleteFeed";
	
	/**
	 * FeedBurner's find feeds API URL.
	 */
	protected final static String FIND_API_URL = BASE_API_URL + "FindFeeds";
	
	/**
	 * FeedBurner's get feed API URL.
	 */
	protected final static String GET_API_URL = BASE_API_URL + "GetFeed";
	
	
	private String username;
	private String password;
	
	/**
	 * Provides a feed element for FeedBurner requests.
	 */
	protected static class FeedElement extends Element {
	
		private static final long serialVersionUID = 1L;

		/**
		 * Creates a new FeedBurner element for the given feed and source url.
		 * 
		 * @param feed
		 * @param sourceUrl
		 */
		protected FeedElement(SyndicationFeed feed, String sourceUrl) {
			
			super("feed");
			
			Element source = new Element("source");
			
			source.setAttribute("url", sourceUrl);
			this.addContent(source);
			
			this.setAttribute("uri", feed.getUrn());
			this.setAttribute("title", feed.getTitle());
		}
		
		
		/**
		 * Returns a string (XML) representation of this element.
		 */
		public String toString() {
			
			return (new XMLOutputter()).outputString(this);
		}
	}
	
	/**
	 * Constructs a new FeedBurner syndication service. 
	 */
	public FeedBurnerService() { }
	
	
	/**
	 * Executes an <code>HTTP GET</code> request to FeedBurner and returns
	 * the response.
	 * 
	 * @param url the API URL for the FeedBurner method to be executed
	 * 
	 * @param data an <code>NameValuePair</code> array containing
	 * query-string parameters to be passed to FeedBurner
	 * 
	 * @return the <code>FeedBurnerResponse</code> to the given API request
	 */
	protected FeedBurnerResponse doGet(String url, NameValuePair[] data) {
		
		GetMethod get = new GetMethod(url);
		get.setQueryString(data);
		
		return execute(get);
	}
	

	/**
	 * Executes an <code>HTTP POST</code> request to FeedBurner and returns
	 * the response.
	 * 
	 * @param url the API URL for the FeedBurner method to be executed
	 * 
	 * @param data an <code>NameValuePair</code> array containing
	 * request body parameters to be passed to FeedBurner
	 * 
	 * @return the <code>FeedBurnerResponse</code> to the given API request
	 */
	protected FeedBurnerResponse doPost(String url, NameValuePair[] data) {
		
		PostMethod post = new PostMethod(url);
        post.setRequestBody(data);
		
        return execute(post);
	}
	
	
	/**
	 * Executes the request contained in the given HTTP method.
	 * 
	 * @param method either an <code>GetMethod</code> or <code>PostMethod</code>
	 * containing an API call to FeedBurner
	 * 
	 * @return the <code>FeedBurnerResponse</code> to the given API request
	 * 
	 * @see #doGet(String, NameValuePair[])
	 * @see #doPost(String, NameValuePair[])
	 */
	protected FeedBurnerResponse execute(HttpMethodBase method) {
		
		HttpClient client = new HttpClient();
		FeedBurnerResponse response = null;
		
        try {
			client.executeMethod(method);
		} catch (HttpException e) {
			throw new FeedBurnerException("Unable to send data to FeedBurner: " + e.getMessage(), e);
		} catch (IOException e) {
			throw new FeedBurnerException("Error communicating with FeedBurner: " + e.getMessage(), e);
		}
		
		try {
			if(method.getResponseContentLength() > 0) {
				response = new FeedBurnerResponse(method.getResponseBodyAsStream());
			}
			 
			System.err.println(response);
			if(response != null && !response.isPositive()) {
				response.raiseException();
			}
		} catch (IOException e) {
			throw new FeedBurnerException("Error parsing FeedBurner response: " + e.getMessage(), e);
		} catch (JDOMException e) {
			throw new FeedBurnerException("Error parsing FeedBurner response: " + e.getMessage(), e);
		}
		
		return response;
	}
	
	
	/**
	 * Adds the given syndication feed to FeedBurner.
	 * 
	 * @param feed the <code>SyndicationFeed</code> to be added
	 * 
	 * @param sourceUrl the publicly available URL from which FeedBurner
	 * should retrieve the feed
	 * 
	 * @return the FeedBurner identifier for the added feed
	 */
	public Long add(SyndicationFeed feed, String sourceUrl) {

		NameValuePair[] data = {
				new NameValuePair("user", username),
				new NameValuePair("password", password),
				new NameValuePair("feed", (new FeedElement(feed, sourceUrl)).toString())
		};
		
		FeedBurnerResponse response = this.doPost(ADD_API_URL, data);
		
		return response != null ? response.getFeedId() : null;
	}

	/**
	 * Deletes the given feed by id.
	 * 
	 * @param feedBurnerFeedId the FeedBurner identifier for the feed to delete
	 */
	public void delete(Long feedBurnerFeedId) {

		NameValuePair[] data = {
				new NameValuePair("user", username),
				new NameValuePair("password", password),
				new NameValuePair("id", String.valueOf(feedBurnerFeedId))
		};

		this.doPost(DELETE_API_URL, data);
	}
	
	/**
	 * Returns true if the given feed exists on Feed Burner.
	 * 
	 * @param feed the <code>SyndicationFeed</code> to check
	 * 
	 * @return <code>true</code> if the given <code>feed</code>
	 * exists on FeedBurner; <code>false</code> otherwise
	 */
	public boolean exists(SyndicationFeed feed) {
		
		FeedBurnerFeed[] feeds = findFeeds();
		
		for(int i = 0; i < feeds.length; i++) {
			if(feed.getUrn().equals(feeds[i].getUri())) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Returns an array of all FeedBurner feeds.
	 * 
	 * @return a <code>FeedBurnerFeed</code> array of all FeedBurner
	 * feeds or an empty array if no feeds exist 
	 */
	public FeedBurnerFeed[] findFeeds() {
		
		NameValuePair[] data = {
		          new NameValuePair("user", username),
		          new NameValuePair("password", password)
		        };
		
		FeedBurnerResponse response = this.doGet(FIND_API_URL, data);
		List feedElements = response.getResponseBody().getRootElement().getChild("feeds").getChildren();
		Element feedElement;
		
		FeedBurnerFeed[] feeds = new FeedBurnerFeed[feedElements.size()];
		FeedBurnerFeed feed;
		
		for(int i = 0; i < feedElements.size(); i++) {
			feedElement = (Element) feedElements.get(i);
			feed = new FeedBurnerFeed();
			
			feed.setId(new Long(feedElement.getAttributeValue("id")));
			feed.setUri(feedElement.getAttributeValue("uri"));
			feed.setTitle(feedElement.getAttributeValue("title"));
			
			feeds[i] = feed;
		}
		
		return feeds;
	}
	
	/**
	 * Returns a FeedBurner response containing the configuration
	 * for the feed identified by the given FeedBurner feed id.
	 * 
	 * @param feedBurnerFeedId the FeedBurner feed id for the feed whose
	 * configuration details should be returned
	 * 
	 * @return a <code>FeedBurnerResponse</code> containing the
	 * configuration details for the feed identified by the given
	 * <code>feedBurnerFeedId</code>
	 */
	public FeedBurnerResponse get(Long feedBurnerFeedId) {
		
		NameValuePair[] data = {
		          new NameValuePair("user", username),
		          new NameValuePair("password", password),
		          new NameValuePair("id", String.valueOf(feedBurnerFeedId))
		        };
		
		
		return this.doGet(GET_API_URL, data);
	}
	
	/**
	 * Sends a request to FeedBurner to resynchronizes 
	 * the given feed from its extnerally available source URL.
	 *  
	 * @param feed the <code>SyndicationFeed</code> to be synchronized
	 */
	public void resync(SyndicationFeed feed) {
		
        NameValuePair[] data = {
          new NameValuePair("user", username),
          new NameValuePair("password", password),
          new NameValuePair("uri", feed.getUrn())
        };
        
        this.doPost(RESYNC_API_URL, data);		
	}

	/**
	 * Sets the FeedBurner user account this service will use.
	 * 
	 * @param username the FeedBurner user account for this service
	 */
	public void setUsername(String username) {
		this.username = username;
	}

	/**
	 * Sets the FeedBurner password for this service.
	 * 
	 * @param password the FeedBurner password for this service
	 */
	public void setPassword(String password) {
		this.password = password;
	}

}
